﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using NUnit.Framework;
using Xamarin.Forms.Internals;

namespace Xamarin.Forms.Core.UnitTests
{
	[TestFixture]
	public class TemplatedItemsListTests : BaseTestFixture
	{
		class MockItemsView
			: ItemsView<BindableObject>
		{
			public MockItemsView()
			{
				TemplatedItems.IsGroupingEnabledProperty = IsGroupingEnabledProperty;
			}

			public event Action<BindableObject> Hooked;
			public event Action<BindableObject> Unhooked;

			public static readonly BindableProperty IsGroupingEnabledProperty =
				BindableProperty.Create<MockItemsView, bool>(lv => lv.IsGroupingEnabled, false);

			public bool IsGroupingEnabled
			{
				get { return (bool)GetValue(IsGroupingEnabledProperty); }
				set { SetValue(IsGroupingEnabledProperty, value); }
			}

			protected override BindableObject CreateDefault(object item)
			{
				return new TextCell { Text = item.ToString() };
			}

			public new TemplatedItemsList<ItemsView<BindableObject>, BindableObject> TemplatedItems
			{
				get { return base.TemplatedItems; }
			}

			BindingBase groupShortNameBinding;
			public BindingBase GroupShortNameBinding
			{
				get { return groupShortNameBinding; }
				set
				{
					if (groupShortNameBinding == value)
						return;
					OnPropertyChanging();
					groupShortNameBinding = value;
					TemplatedItems.GroupShortNameBinding = value;
					OnPropertyChanged();
				}
			}

			BindingBase groupDisplayBinding;
			public BindingBase GroupDisplayBinding
			{
				get { return groupDisplayBinding; }
				set
				{
					if (groupDisplayBinding == value)
						return;

					OnPropertyChanging();
					groupDisplayBinding = value;
					TemplatedItems.GroupDisplayBinding = value;
					OnPropertyChanged();
				}
			}

			protected override void SetupContent(BindableObject content, int index)
			{
				base.SetupContent(content, index);

				if (Hooked != null)
					Hooked(content);
			}

			protected override void UnhookContent(BindableObject content)
			{
				base.UnhookContent(content);

				if (Unhooked != null)
					Unhooked(content);
			}
		}

		[SetUp]
		public override void Setup()
		{
			base.Setup();
			bindable = new MockItemsView();

			Device.PlatformServices = new MockPlatformServices();
		}

		[TearDown]
		public override void TearDown()
		{
			base.TearDown();
			Device.PlatformServices = null;
		}

		MockItemsView bindable;

		[Test]
		public void ListProxyNotNullWithNullItemsSource()
		{
			Assert.IsNotNull(bindable.TemplatedItems.ListProxy);
		}

		[Test]
		public void ResetOnItemsSourceChanged()
		{
			bool raised = false;
			NotifyCollectionChangedAction action = default(NotifyCollectionChangedAction);
			bindable.TemplatedItems.CollectionChanged += (sender, args) =>
			{
				raised = true;
				action = args.Action;
			};

			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, Enumerable.Empty<string>());

			Assert.IsTrue(raised, "CollectionChanged was not raised");
			Assert.AreEqual(NotifyCollectionChangedAction.Reset, action);
		}
		/*
		[Test]
		public void ResetOnInfiniteScrollingChanged()
		{
			bool raised = false;
			NotifyCollectionChangedAction action = default(NotifyCollectionChangedAction);
			bindable.TemplatedItems.CollectionChanged += (sender, args) => {
				raised = true;
				action = args.Action;
			};

			bindable.SetValue (ItemsView.InfiniteScrollingProperty, true);

			Assert.IsTrue (raised, "CollectionChanged was not raised");
			Assert.AreEqual (NotifyCollectionChangedAction.Reset, action);
		}*/

		[Test]
		public void ResetOnTemplateChanged()
		{
			// Template changes won't trigger a reset if there's no items.
			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, new[] { "Foo" });

			bool raised = false;
			NotifyCollectionChangedAction action = default(NotifyCollectionChangedAction);
			bindable.TemplatedItems.CollectionChanged += (sender, args) =>
			{
				raised = true;
				action = args.Action;
			};

			bindable.SetValue(ItemsView<BindableObject>.ItemTemplateProperty, new DataTemplate());

			Assert.IsTrue(raised, "CollectionChanged was not raised");
			Assert.AreEqual(NotifyCollectionChangedAction.Reset, action);
		}

		[Test]
		public void PassThroughChanges()
		{
			var collection = new ObservableCollection<string>();
			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, collection);

			bool raised = false;
			NotifyCollectionChangedEventArgs args = null;
			bindable.TemplatedItems.CollectionChanged += (sender, eventArgs) =>
			{
				raised = true;
				args = eventArgs;
			};

			string str = "foo bar";
			collection.Add(str);

			Assert.IsTrue(raised, "CollectionChanged was not raised");
			Assert.AreEqual(NotifyCollectionChangedAction.Add, args.Action);
			Assert.AreEqual(0, args.NewStartingIndex);
			Assert.IsNotNull(args.NewItems);
			Assert.AreEqual(1, args.NewItems.Count);
			Assert.AreSame(str, ((Cell)args.NewItems[0]).BindingContext);
		}

		[Test]
		public void ListProxyUpdatesWithItemsSource()
		{
			var collection = new List<string> { "foo bar" };
			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, collection);

			Assert.IsNotNull(bindable.TemplatedItems.ListProxy);
			Assert.AreEqual(collection.Count, bindable.TemplatedItems.ListProxy.Count);
		}

		[Test]
		public void GetOrCreateContent([Values(0, 1, 2)] int index)
		{
			var collection = new List<string> { "foo", "bar", "baz" };
			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, collection);

			DataTemplate template = new DataTemplate(typeof(TextCell));
			template.SetBinding(TextCell.TextProperty, new Binding("."));
			bindable.SetValue(ItemsView<BindableObject>.ItemTemplateProperty, template);

			BindableObject content = bindable.TemplatedItems.GetOrCreateContent(index, collection[index]);
			Assert.IsNotNull(content);

			TextCell textCell = content as TextCell;
			Assert.IsNotNull(textCell, "Content was did not match the template type, expected {0} but got {1}", typeof(TextCell), content.GetType());

			Assert.AreSame(collection[index], textCell.BindingContext);
			Assert.AreSame(collection[index], textCell.Text);

			BindableObject content2 = bindable.TemplatedItems.GetOrCreateContent(index, collection[index]);
			Assert.AreSame(content, content2);
		}

		[Test]
		public void GetOrCreateContentDefault()
		{
			Assert.IsNull(bindable.GetValue(ItemsView<BindableObject>.ItemTemplateProperty));

			var collection = new List<string> { "foo", "bar", "baz" };
			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, collection);

			const int index = 0;
			BindableObject content = bindable.TemplatedItems.GetOrCreateContent(index, collection[index]);
			Assert.IsNotNull(content);

			TextCell textCell = content as TextCell;
			Assert.IsNotNull(textCell, "Content was did not match the template type, expected {0} but got {1}", typeof(TextCell), content.GetType());

			Assert.AreSame(collection[index], textCell.BindingContext);
			Assert.AreSame(collection[index], textCell.Text);

			BindableObject content2 = bindable.TemplatedItems.GetOrCreateContent(index, collection[index]);
			Assert.AreSame(content, content2);
		}

		[Test]
		public void GetOrCreateContentAfterTemplateChange()
		{
			var collection = new List<string> { "foo", "bar", "baz" };
			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, collection);

			const int index = 0;

			DataTemplate template = new DataTemplate(typeof(TextCell));
			template.SetBinding(TextCell.TextProperty, new Binding("."));
			bindable.SetValue(ItemsView<BindableObject>.ItemTemplateProperty, template);

			BindableObject content = bindable.TemplatedItems.GetOrCreateContent(index, collection[index]);
			Assert.That(content, Is.InstanceOf<TextCell>());

			template = new DataTemplate(typeof(SwitchCell));
			template.SetBinding(SwitchCell.TextProperty, new Binding("."));

			bindable.SetValue(ItemsView<BindableObject>.ItemTemplateProperty, template);

			BindableObject content2 = bindable.TemplatedItems.GetOrCreateContent(index, collection[index]);
			Assert.IsNotNull(content2);
			Assert.That(content2, Is.InstanceOf<SwitchCell>());

			var switchCell = (SwitchCell)content2;
			Assert.AreSame(collection[index], switchCell.BindingContext);
		}

		[Test]
		public void GetOrCreateContentAfterItemsSourceChanged()
		{
			var collection = new List<string> { "foo", "bar", "baz" };
			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, collection);

			const int index = 0;

			DataTemplate template = new DataTemplate(typeof(TextCell));
			template.SetBinding(TextCell.TextProperty, new Binding("."));
			bindable.SetValue(ItemsView<BindableObject>.ItemTemplateProperty, template);

			BindableObject content = bindable.TemplatedItems.GetOrCreateContent(index, collection[index]);
			Assert.IsNotNull(content);

			collection = new List<string> { "we", "wee", "weee" };
			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, collection);

			var content2 = (TextCell)bindable.TemplatedItems.GetOrCreateContent(index, collection[index]);

			Assert.AreNotSame(content, content2);
			Assert.AreSame(collection[index], content2.BindingContext);
			Assert.AreSame(collection[index], content2.Text);
		}

		/*[Test]
		public void GetOrCreateContentAfterInfiniteScrollingChanged()
		{
			var collection = new List<string> { "foo", "bar", "baz" };
			bindable.SetValue (ItemsView.ItemsSourceProperty, collection);

			const int index = 0;

			DataTemplate template = new DataTemplate (typeof (TextCell));
			template.SetBinding (TextCell.TextProperty, new Binding ("."));
			bindable.SetValue (ItemsView.ItemTemplateProperty, template);

			BindableObject content = bindable.TemplatedItems.GetOrCreateContent (index, collection[index], o => new TextCell (o.ToString()));
			Assert.IsNotNull (content);

			bindable.SetValue (ItemsView.InfiniteScrollingProperty, true);

			var content2 = (TextCell)bindable.TemplatedItems.GetOrCreateContent (index, collection[index], o => new TextCell (o.ToString()));

			Assert.AreNotSame (content, content2);
			Assert.AreSame (collection[index], content2.BindingContext);
			Assert.AreSame (collection[index], content2.Text);
		}*/

		[Test]
		[Description("Make sure we're not duplicate cell instances for an equal item if it's a different index")]
		public void GetOrCreateContentEqualItemDifferentItemDifferentIndex()
		{
			Assert.IsNull(bindable.GetValue(ItemsView<BindableObject>.ItemTemplateProperty));

			var collection = new List<string> { "foo", "foo" };
			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, collection);

			BindableObject content = bindable.TemplatedItems.GetOrCreateContent(0, collection[0]);
			Assert.IsNotNull(content);

			TextCell textCell = content as TextCell;
			Assert.IsNotNull(textCell, "Content was did not match the template type, expected {0} but got {1}", typeof(TextCell), content.GetType());

			Assert.AreSame(collection[0], textCell.BindingContext);
			Assert.AreSame(collection[0], textCell.Text);

			BindableObject content2 = bindable.TemplatedItems.GetOrCreateContent(1, collection[1]);
			Assert.AreNotSame(content, content2);

			Assert.AreSame(collection[1], textCell.BindingContext);
			Assert.AreSame(collection[1], textCell.Text);
		}

		[Test]
		[Description("Make sure we're not duplicate cell instances for the same item if it's a different index")]
		public void GetOrCreateContentEqualItemSameItemDifferentIndex()
		{
			Assert.IsNull(bindable.GetValue(ItemsView<BindableObject>.ItemTemplateProperty));

			string item = "foo";

			var collection = new List<string> { item, item };
			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, collection);

			BindableObject content = bindable.TemplatedItems.GetOrCreateContent(0, item);
			Assert.IsNotNull(content);

			TextCell textCell = content as TextCell;
			Assert.IsNotNull(textCell, "Content was did not match the template type, expected {0} but got {1}", typeof(TextCell), content.GetType());

			Assert.AreSame(item, textCell.BindingContext);
			Assert.AreSame(item, textCell.Text);

			BindableObject content2 = bindable.TemplatedItems.GetOrCreateContent(1, item);
			Assert.AreNotSame(content, content2);

			Assert.AreSame(item, textCell.BindingContext);
			Assert.AreSame(item, textCell.Text);
		}

		[Test]
		public void ItemsSourceInsertPreRealzied()
		{
			var collection = new ObservableCollection<string> { "foo", "bar" };
			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, collection);

			collection.Insert(1, "baz");

			Assert.That(bindable.TemplatedItems.Count, Is.EqualTo(3));
			Assert.That(bindable.TemplatedItems.GetOrCreateContent(0, collection[0]).BindingContext, Is.SameAs(collection[0]));
			Assert.That(bindable.TemplatedItems.GetOrCreateContent(1, collection[1]).BindingContext, Is.SameAs(collection[1]));
			Assert.That(bindable.TemplatedItems.GetOrCreateContent(2, collection[2]).BindingContext, Is.SameAs(collection[2]));
		}

		[Test]
		public void ItemsSourceInsertPostRealized()
		{
			var collection = new ObservableCollection<string> { "foo", "bar" };
			bindable.SetValue(ItemsView<BindableObject>.ItemsSourceProperty, collection);

			// Force the handler to realize/create the content
			bindable.TemplatedItems.GetOrCreateContent(0, collection[0]);
			bindable.TemplatedItems.GetOrCreateContent(1, collection[1]);

			collection.Insert(1, "baz");

			Assert.That(bindable.TemplatedItems.Count, Is.EqualTo(3));
			Assert.That(bindable.TemplatedItems.GetOrCreateContent(0, collection[0]).BindingContext, Is.SameAs(collection[0]));
			Assert.That(bindable.TemplatedItems.GetOrCreateContent(1, collection[1]).BindingContext, Is.SameAs(collection[1]));
			Assert.That(bindable.TemplatedItems.GetOrCreateContent(2, collection[2]).BindingContext, Is.SameAs(collection[2]));
		}

		[TestCase(0, 0, 0)]
		[TestCase(3, 1, 0)]
		public void GetGroupIndexFromGlobalGroupIndex(int globalIndex, int expectedIndex, int expectedLeftOver)
		{
			var collection = new[] {
				new[] { "foo", "fad" },
				new[] { "bar", "baz" }
			};
			bindable.ItemsSource = collection;
			bindable.IsGroupingEnabled = true;

			int leftOver;
			int index = bindable.TemplatedItems.GetGroupIndexFromGlobal(globalIndex, out leftOver);
			Assert.That(index, Is.EqualTo(expectedIndex));
			Assert.That(leftOver, Is.EqualTo(expectedLeftOver));
		}

		[TestCase(1, 0, 1)]
		[TestCase(2, 0, 2)]
		[TestCase(4, 1, 1)]
		[TestCase(5, 1, 2)]
		public void GetGroupIndexFromGlobalItemIndex(int globalIndex, int expectedIndex, int expectedLeftOver)
		{
			var collection = new[] {
				new[] { "foo", "fad" },
				new[] { "bar", "baz" }
			};
			bindable.ItemsSource = collection;
			bindable.IsGroupingEnabled = true;

			int leftOver;
			int index = bindable.TemplatedItems.GetGroupIndexFromGlobal(globalIndex, out leftOver);
			Assert.That(index, Is.EqualTo(expectedIndex));
			Assert.That(leftOver, Is.EqualTo(expectedLeftOver));
		}

		[Test]
		public void GetGroupAndIndexOfItem([Values(0, 1, 2)] int group, [Values(0, 1, 2)] int index)
		{
			var collection = new[] {
				new[] { "foo", "fad" },
				new[] { "bar", "baz" }
			};
			bindable.ItemsSource = collection;
			bindable.IsGroupingEnabled = true;

			object item = null;
			if (group < collection.Length)
			{
				if (index < collection[group].Length)
					item = collection[group][index];
			}

			if (item == null)
			{
				item = "not in here";
				group = index = -1;
			}

			var location = bindable.TemplatedItems.GetGroupAndIndexOfItem(item);
			Assert.That(location.Item1, Is.EqualTo(group), "Group index was incorrect");
			Assert.That(location.Item2, Is.EqualTo(index), "Item index was incorrect");
		}

		[Test]
		public void GetGroupAndIndexOfItemNotGrouped()
		{
			var items = Enumerable.Range(0, 10).ToList();
			bindable.ItemsSource = items;

			var location = bindable.TemplatedItems.GetGroupAndIndexOfItem(null, items[2]);

			Assert.That(location.Item1, Is.EqualTo(0));
			Assert.That(location.Item2, Is.EqualTo(2));
		}

		[Test]
		public void ItemsSourcePropertyChangedWithBindable()
		{
			bool raised = false;
			bindable.TemplatedItems.PropertyChanged += (s, e) =>
			{
				if (e.PropertyName == "ItemsSource")
					raised = true;
			};

			bindable.ItemsSource = new object[0];

			Assert.That(raised, Is.True, "INPC not raised for ItemsSource");
		}

		[Test]
		public void IndexCorrectAfterMovingGroups()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.IsGroupingEnabled = true;

			items.Move(0, 1);

			var til = bindable.TemplatedItems.GetGroup(1);
			int index = GetIndex(til.HeaderContent);

			Assert.That(index, Is.EqualTo(1));
		}

		[Test]
		public void ShortNameSetBeforeGrouping()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupDisplayBinding = new Binding(".[0]");
			bindable.GroupShortNameBinding = new Binding(".[0]");
			bindable.IsGroupingEnabled = true;

			Assert.That(bindable.TemplatedItems.ShortNames[0], Is.EqualTo("foo"));
		}

		[Test]
		public void ItemAddedWithShortNameSetButUngrouped()
		{
			var items = new ObservableCollection<string> { "foo", "bar" };

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");

			Assert.That(() => items.Add("baz"), Throws.Nothing);
		}

		[Test]
		public void ItemAddedWithShortNameSetButGroupingDisabled()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;
			bindable.IsGroupingEnabled = false;

			Assert.That(() => items.Add(new ObservableCollection<string>()), Throws.Nothing);
		}

		int GetIndex(BindableObject item)
		{
			return TemplatedItemsList<ItemsView<BindableObject>, BindableObject>.GetIndex(item);
		}

		[Test]
		public void GetIndex()
		{
			var items = new List<string> { "foo", "bar", "baz" };

			bindable.ItemsSource = items;

			BindableObject item = bindable.TemplatedItems.GetOrCreateContent(1, items[1]);
			int index = GetIndex(item);
			Assert.That(index, Is.EqualTo(1));
		}

		[Test]
		public void GetIndexAfterInsert()
		{
			var items = new ObservableCollection<string> { "foo", "bar", "baz" };

			bindable.ItemsSource = items;

			BindableObject originalItem = bindable.TemplatedItems.GetOrCreateContent(1, items[1]);

			items.Insert(1, "fad");

			BindableObject item = bindable.TemplatedItems.GetOrCreateContent(1, items[1]);

			int index = GetIndex(item);
			Assert.That(index, Is.EqualTo(1));

			index = GetIndex(originalItem);
			Assert.That(index, Is.EqualTo(2));
		}

		[Test]
		public void GetIndexAfterMove()
		{
			var items = new ObservableCollection<string> { "foo", "bar", "baz" };

			bindable.ItemsSource = items;

			BindableObject item0 = bindable.TemplatedItems.GetOrCreateContent(0, items[0]);
			BindableObject item1 = bindable.TemplatedItems.GetOrCreateContent(1, items[1]);
			BindableObject item2 = bindable.TemplatedItems.GetOrCreateContent(2, items[2]);

			items.Move(0, 2); // foo, bar, baz becomes bar (1), baz (2), foo (0)

			Assert.That(GetIndex(item0), Is.EqualTo(2));
			Assert.That(GetIndex(item1), Is.EqualTo(0));
			Assert.That(GetIndex(item2), Is.EqualTo(1));
		}

		[Test]
		public void GetIndexAfterRemove()
		{
			var items = new ObservableCollection<string> { "foo", "bar", "baz" };

			bindable.ItemsSource = items;

			BindableObject item1 = bindable.TemplatedItems.GetOrCreateContent(1, items[1]);
			BindableObject item2 = bindable.TemplatedItems.GetOrCreateContent(2, items[2]);

			items.RemoveAt(0);

			Assert.That(GetIndex(item1), Is.EqualTo(0));
			Assert.That(GetIndex(item2), Is.EqualTo(1));
		}

		[Test]
		public void GetGroup()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;

			var til = bindable.TemplatedItems.GetGroup(1);
			var item = til[1];

			var group = TemplatedItemsList<ItemsView<BindableObject>, BindableObject>.GetGroup(item);

			Assert.That(group, Is.SameAs(til));
		}

		[Test]
		public void GetIndexGrouped()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;

			var til = bindable.TemplatedItems.GetGroup(1);
			var item = til[1];

			int index = GetIndex(item);
			Assert.That(index, Is.EqualTo(1));
		}

		[Test]
		public void GetGroupAndIndexOfItem()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;

			var result = bindable.TemplatedItems.GetGroupAndIndexOfItem(items[1], items[1][1]);

			Assert.That(result.Item1, Is.EqualTo(1));
			Assert.That(result.Item2, Is.EqualTo(1));
		}

		[Test]
		public void GetGroupAndIndexOfItemNoGroup()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;

			var result = bindable.TemplatedItems.GetGroupAndIndexOfItem(null, items[1][1]);

			Assert.That(result.Item1, Is.EqualTo(1));
			Assert.That(result.Item2, Is.EqualTo(1));
		}

		[Test]
		public void GetGroupAndIndexOfItemNotFound()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;

			var group = new ObservableCollection<string> { "bam" };

			var result = bindable.TemplatedItems.GetGroupAndIndexOfItem(group, group[0]);

			Assert.That(result.Item1, Is.EqualTo(-1));
			Assert.That(result.Item2, Is.EqualTo(-1));
		}

		[Test]
		public void GetGroupAndIndexOfItemItemNotFound()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;

			var result = bindable.TemplatedItems.GetGroupAndIndexOfItem(items[1], "bam");

			Assert.That(result.Item1, Is.EqualTo(1));
			Assert.That(result.Item2, Is.EqualTo(-1));
		}

		[Test]
		[Description("Issue #2464: ANE thrown when moving items in a ListView")]
		public void MovingPastRealizedWindowAndBackDoesntThrow()
		{
			var items = new ObservableCollection<string>(Enumerable.Range(0, 100).Select(i => i.ToString()));

			bindable.ItemsSource = items;

			bindable.TemplatedItems.GetOrCreateContent(0, items[0]);
			bindable.TemplatedItems.GetOrCreateContent(1, items[1]);

			items.Move(0, 3);
			Assert.That(() => items.Move(3, 2), Throws.Nothing);

			Assert.That(GetIndex(bindable.TemplatedItems[0]), Is.EqualTo(0));
			Assert.That(GetIndex(bindable.TemplatedItems[1]), Is.EqualTo(1));
			Assert.That(GetIndex(bindable.TemplatedItems[2]), Is.EqualTo(2));
			Assert.That(GetIndex(bindable.TemplatedItems[3]), Is.EqualTo(3));
		}

		[Test]
		public void GetGlobalIndexOfItem()
		{
			var items = new ObservableCollection<string>(Enumerable.Range(0, 100).Select(i => i.ToString()));
			bindable.ItemsSource = items;

			int global = bindable.TemplatedItems.GetGlobalIndexOfItem("50");
			Assert.That(global, Is.EqualTo(50));
		}

		[Test]
		public void GetGlobalIndexOfItemNotFound()
		{
			var items = new ObservableCollection<string>(Enumerable.Range(0, 100).Select(i => i.ToString()));
			bindable.ItemsSource = items;

			int global = bindable.TemplatedItems.GetGlobalIndexOfItem("101");
			Assert.That(global, Is.EqualTo(-1));
		}

		[Test]
		public void GetGlobalIndexOfItemGrouped()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;

			int global = bindable.TemplatedItems.GetGlobalIndexOfItem("baz");
			Assert.That(global, Is.EqualTo(5));
		}

		[Test]
		public void GetGlobalIndexOfItemGroupedNotFound()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;

			int global = bindable.TemplatedItems.GetGlobalIndexOfItem("101");
			Assert.That(global, Is.EqualTo(-1));
		}

		[Test]
		public void GetGlobalIndexOfGroupItemGrouped()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "baz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;

			int global = bindable.TemplatedItems.GetGlobalIndexOfItem(items[1], "baz");
			Assert.That(global, Is.EqualTo(5));
		}

		[Test]
		public void GetGlobalIndexOfGroupItemGroupedNotFound()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "foo", "faz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;

			int global = bindable.TemplatedItems.GetGlobalIndexOfItem(items[1], "bar");
			Assert.That(global, Is.EqualTo(-1));
		}

		[Test]
		public void GetGlobalIndexOfGroupItemGroupedGroupNotFound()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "foo", "faz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;

			int global = bindable.TemplatedItems.GetGlobalIndexOfItem(new object(), "foo");
			Assert.That(global, Is.EqualTo(-1));
		}

		[Test]
		public void SetupContentOnCreation()
		{
			var items = new ObservableCollection<string> {
				"Foo",
				"Bar"
			};

			bindable.ItemTemplate = new DataTemplate(typeof(TextCell))
			{
				Bindings = {
					{ TextCell.TextProperty, new Binding (".") }
				}
			};

			int count = 0;
			bindable.Hooked += obj =>
			{
				count++;
				Assert.That(obj, Is.InstanceOf(typeof(TextCell)));
				Assert.That(items, Contains.Item(obj.BindingContext));
			};

			bindable.ItemsSource = items;

			bindable.TemplatedItems.GetOrCreateContent(0, items[0]);
			bindable.TemplatedItems.GetOrCreateContent(1, items[1]);

			Assert.That(count, Is.EqualTo(2));
		}

		[Test]
		public void UnhookGroupOnRemoval()
		{
			var inner = new ObservableCollection<string> {
				"Foo",
			};

			var items = new ObservableCollection<ObservableCollection<string>> {
				inner
			};

			bindable.IsGroupingEnabled = true;
			bindable.GroupDisplayBinding = new Binding(".[0]");

			bindable.ItemTemplate = new DataTemplate(typeof(TextCell))
			{
				Bindings = {
					{ TextCell.TextProperty, new Binding (".") }
				}
			};

			int count = 0;
			bindable.Unhooked += obj =>
			{
				if (count == 0)
				{
					Assert.That(obj.BindingContext, Is.InstanceOf(typeof(TemplatedItemsList<ItemsView<BindableObject>, BindableObject>)));
					Assert.That(((TemplatedItemsList<ItemsView<BindableObject>, BindableObject>)obj.BindingContext).ListProxy.ProxiedEnumerable, Is.SameAs(inner));
				}
				else
				{
					Assert.That(obj.BindingContext, Is.SameAs(inner[0]));
				}

				count++;
			};

			bindable.ItemsSource = items;

			var til = bindable.TemplatedItems.GetGroup(0);
			til.GetOrCreateContent(0, inner[0]);

			Assume.That(til, Is.Not.Null);
			Assume.That(til.HeaderContent, Is.Not.Null);
			Assume.That(count, Is.EqualTo(0));

			items.RemoveAt(0);

			Assert.That(count, Is.EqualTo(2));
		}

		[Test]
		public void HookAndUnhookGroupOnReplace()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> {
					"Foo",
				}
			};

			bindable.IsGroupingEnabled = true;
			bindable.GroupDisplayBinding = new Binding(".[0]");
			bindable.ItemTemplate = new DataTemplate(typeof(TextCell))
			{
				Bindings = {
					{ TextCell.TextProperty, new Binding (".") }
				}
			};

			bindable.ItemsSource = items;

			var til = bindable.TemplatedItems.GetGroup(0);
			til.GetOrCreateContent(0, items[0][0]);

			Assume.That(til, Is.Not.Null);
			Assume.That(til.HeaderContent, Is.Not.Null);

			int hcount = 0;
			bindable.Hooked += obj =>
			{
				Assert.That(obj.BindingContext, Is.InstanceOf(typeof(TemplatedItemsList<ItemsView<BindableObject>, BindableObject>)));
				hcount++;
			};

			int ucount = 0;
			bindable.Unhooked += obj =>
			{
				Assert.That(obj.BindingContext,
					Is.InstanceOf(typeof(TemplatedItemsList<ItemsView<BindableObject>, BindableObject>))
						.Or.EqualTo("Foo"));

				ucount++;
			};

			items[0] = new ObservableCollection<string> { "Baz" };

			Assert.That(hcount, Is.EqualTo(1));
			Assert.That(ucount, Is.EqualTo(2));
		}

		[Test]
		public void UnhookContentOnRemoval()
		{
			var items = new ObservableCollection<string> {
				"Foo",
				"Bar"
			};

			bindable.ItemTemplate = new DataTemplate(typeof(TextCell))
			{
				Bindings = {
					{ TextCell.TextProperty, new Binding (".") }
				}
			};

			int count = 0;
			bindable.Unhooked += obj =>
			{
				count++;
				Assert.That(obj, Is.InstanceOf(typeof(TextCell)));
				Assert.That(obj.BindingContext, Is.EqualTo("Foo"));
			};

			bindable.ItemsSource = items;

			bindable.TemplatedItems.GetOrCreateContent(0, items[0]);
			bindable.TemplatedItems.GetOrCreateContent(1, items[1]);

			Assume.That(count, Is.EqualTo(0));

			items.RemoveAt(0);

			Assert.That(count, Is.EqualTo(1));
		}

		[Test]
		public void HookAndUnhookContentOnReplace()
		{
			var items = new ObservableCollection<string> {
				"Foo",
				"Bar"
			};

			bindable.ItemTemplate = new DataTemplate(typeof(TextCell))
			{
				Bindings = {
					{ TextCell.TextProperty, new Binding (".") }
				}
			};

			bindable.ItemsSource = items;

			bindable.TemplatedItems.GetOrCreateContent(0, items[0]);
			bindable.TemplatedItems.GetOrCreateContent(1, items[1]);

			int hcount = 0;
			bindable.Hooked += obj =>
			{
				hcount++;
				Assert.That(obj, Is.InstanceOf(typeof(TextCell)));
				Assert.That(obj.BindingContext, Is.EqualTo("Baz"));
			};

			int ucount = 0;
			bindable.Unhooked += obj =>
			{
				ucount++;
				Assert.That(obj, Is.InstanceOf(typeof(TextCell)));
				Assert.That(obj.BindingContext, Is.EqualTo("Foo"));
			};

			items[0] = "Baz";

			Assert.That(hcount, Is.EqualTo(1));
			Assert.That(ucount, Is.EqualTo(1));
		}

		[Test(Description = "If the cell exists and has an index, we still need to check if it's in the group asked for")]
		public void IndexOfFailsForCellInAnotherGroup()
		{
			var items = new ObservableCollection<ObservableCollection<string>> {
				new ObservableCollection<string> { "foo", "faz" },
				new ObservableCollection<string> { "bar", "baz" }
			};

			bindable.ItemsSource = items;
			bindable.GroupShortNameBinding = new Binding(".");
			bindable.IsGroupingEnabled = true;

			var group1 = bindable.TemplatedItems.GetGroup(0);
			var cell = group1[1];

			Assume.That(group1.IndexOf(cell), Is.EqualTo(1));

			var group2 = bindable.TemplatedItems.GetGroup(1);

			Assert.That(group2.IndexOf(cell), Is.EqualTo(-1));
		}
	}
}
